iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Software Development

30 天學會做一個 CLI:打造人類與 AI 都友善的現代 CLI 應用系列 第 26 篇

CLI 編輯器的檔案瀏覽:載入檔案與 Viewport

  • 分享至 

  • xImage
  •  

上一篇已經讓編輯器可以顯示畫面並讀取方向鍵。這一篇將開啟實際的文字檔,把內容載入記憶體,再用 viewport 決定終端機目前顯示哪幾行。完成後,程式可以開啟文字檔。游標移到畫面底端後繼續按 ↓,將內容向下捲動。

將檔案載入 rows

程式開啟檔案時,會把檔案內容載入記憶體。editor 用 rows 保存每一行文字,filename 保存檔案名稱,供狀態列顯示:

type editor struct {
	rows     []string // 每個元素保存一行文字。
	filename string
}

rows[0] 是檔案第 1 行,rows[120] 是第 121 行。使用行陣列後,編輯器可以直接用行號取得游標所在行或畫面要顯示的幾行。

openFile 使用 bufio.Scanner 逐行讀取檔案:

func (e *editor) openFile(name string) error {
	f, err := os.Open(name)
	if err != nil {
		return err
	}
	defer f.Close()

	e.filename = name
	sc := bufio.NewScanner(f)
	sc.Buffer(make([]byte, 1024*1024), 1024*1024)
	for sc.Scan() {
		// Scanner 每次回傳一行,內容不包含換行符。
		e.rows = append(e.rows, sc.Text())
	}
	return sc.Err()
}

用 Viewport 瀏覽超出畫面的檔案

當檔案的行數超過畫面高度時,終端機一次只能顯示其中一部分。例如用 24 行高的畫面瀏覽 500 行檔案時,rowOffset 會記錄畫面第一行對應到檔案哪一行。

假設 rowOffset == 100:

螢幕第 1 行顯示檔案第 101 行
螢幕第 2 行顯示檔案第 102 行

程式從 0 開始計算位置,因此螢幕行 screenY 對應的檔案行是:

fileRow = rowOffset + screenY

cy 記錄游標位於檔案第幾行。當游標移動超出目前畫面範圍時,scroll 會更新 rowOffset 調整 Viewport:

func (e *editor) scroll() {
	// 游標移到目前畫面上方時,讓該檔案行成為畫面第一行。
	if e.cy < e.rowOffset {
		e.rowOffset = e.cy
	}
	// 游標移到目前畫面下方時,讓該檔案行成為畫面最後一行。
	if e.cy >= e.rowOffset+e.screenRows {
		e.rowOffset = e.cy - e.screenRows + 1
	}
}

drawRows 使用 rowOffset 將螢幕行號換算成檔案行號,決定每個螢幕行繪製哪一行內容:

func (e *editor) drawRows(b *bytes.Buffer) {
	// 依序處理終端機中可用的每一行。
	for screenY := 0; screenY < e.screenRows; screenY++ {
		// 把螢幕行號換算成檔案行號。
		fileRow := e.rowOffset + screenY
		if fileRow >= len(e.rows) {
			// 超出檔案結尾的畫面行顯示 ~。
			b.WriteString("~")
		} else {
			// 將對應的檔案行加入畫面 buffer。
			b.WriteString(e.rows[fileRow])
		}
		// 清除舊內容的行尾,再移到下一個螢幕行。
		b.WriteString("\x1b[K\r\n")
	}
}

每次按鍵處理完後,主迴圈會呼叫 refreshScreen。它先透過 scroll 更新 Viewport 偏移量,接著讓 drawRows 繪製對應行,最後將檔案游標座標換算為終端機螢幕座標:

func (e *editor) refreshScreen() {
	e.scroll()

	var b bytes.Buffer
	b.WriteString("\x1b[?25l\x1b[H") // 隱藏游標並移回左上角
	e.drawRows(&b)
	e.drawStatusBar(&b)

	// 把檔案座標 (cx, cy) 扣除 Viewport 偏移量,換算成終端機螢幕座標
	fmt.Fprintf(&b, "\x1b[%d;%dH", e.cy-e.rowOffset+1, e.cx-e.colOffset+1)
	b.WriteString("\x1b[?25h")
	os.Stdout.Write(b.Bytes())
}

在 cli-sample/go-editor-tutorial/ 執行:

go run ./open-file-viewport testdata/lines.txt

程式啟動後會進入全螢幕,畫面顯示 testdata/lines.txt 的前幾行內容,底部的反白狀態列顯示檔名、總行數與目前游標位置:

line 001: opening a file
line 002: reading one line at a time
...
line 023: one more line
 testdata/lines.txt - 201 lines                            1/201 

持續按下 ↓ 將游標移至畫面最底端後,再按一次 ↓,scroll 會增加 rowOffset,畫面隨即向下捲動顯示 line 024,狀態列游標行數同步更新為 24/201。按 Ctrl+Q 即可結束程式。

下一篇會在 rows 上加入文字修改,並把修改後的內容存回磁碟。


上一篇
CLI 編輯器的畫面控制:清除、游標移動與重繪
下一篇
CLI 編輯器的文字編輯:增刪、存檔與 Undo/Redo
系列文
30 天學會做一個 CLI:打造人類與 AI 都友善的現代 CLI 應用 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言